/*
* Copyright (C) Microsoft. All rights reserved.  
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not 
* use this file except in compliance with the License.  You may obtain a copy 
* of the License at http://www.apache.org/licenses/LICENSE-2.0  
* 
* THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED 
* WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, 
* MERCHANTABLITY OR NON-INFRINGEMENT. 
*
* See the Apache License, Version 2.0 for specific language governing 
* permissions and limitations under the License.
*
*/
#pragma once
#ifdef BAJA_WINRT
#include <ppltasks.h>
#endif

#include <baja/common.hpp>
#include <baja/error.hpp>
#include <baja/stream/stream_fwd.hpp>
#include <baja/stream/stream_types.hpp>
#include <baja/stream/stream_interfaces.hpp>

namespace baja { 

class memory_stream : public imemory_stream  
{
private:
    std::vector<byte> _buffer;
    size_t _curPos;

private:

    void initialize(const void* buffer, size_t size)
    {
        if (buffer == nullptr)
        {
            if (size > 0)
            {
                BAJA_THROW(std::invalid_argument("if buffer is null size must be zero"));
            }
            else
            {
                return; // nothing to initialize with
            }
        }

        _buffer.resize(size);

        std::memcpy(_buffer.data(), buffer, size);
    }

public:
    memory_stream() : _curPos(0) {}
    static std::shared_ptr<memory_stream> create(const void* buffer, size_t size)
    {
        std::shared_ptr<memory_stream> returnValue = std::make_shared<memory_stream>();
        returnValue->initialize(buffer, size);
        return returnValue;
    }

    size_t read(void* buffer, const size_t bytesToRead)
    {
        BAJA_CHECK_ARG(buffer);
        BAJA_CHECK_ARG(bytesToRead > 0);
        
        size_t bytesRemaining = _buffer.size() - _curPos;

        size_t bytesToCopy = min(bytesRemaining, bytesToRead);

        // Copy the data
        std::memcpy(buffer, _buffer.data() + _curPos, bytesToCopy);

        // Adjust the current position
        _curPos += bytesToCopy;

        return bytesToCopy;
    }

    size_t write(const void* buffer, const size_t bytesToWrite)
    {
        BAJA_CHECK_ARG(buffer);
        BAJA_CHECK_ARG(bytesToWrite > 0);

        size_t bytesRemaining = _buffer.size() - _curPos;

        // First, make sure there's enough room for the new data
        if (bytesToWrite > bytesRemaining)
        {
            size_t bytesNeeded = bytesToWrite - bytesRemaining;
            
            // Reallocate to the needed size if possible
            this->setSize(_buffer.size() + bytesNeeded);
        }

        // Copy the data
        std::memcpy(_buffer.data() + _curPos, buffer, bytesToWrite);

        // Adjust the current position
        _curPos += bytesToWrite;

        return bytesToWrite;
    }

    void setSize(const size_t size)
    {
        _buffer.resize(size);
    }

    size_t getSize()
    {
        return _buffer.size();
    }

    size_t seek(const long long offset, const stream_origin origin)
    {
        size_t newPos = 0;

        if (offset > MAXDWORD32)
            BAJA_THROW(std::logic_error("offset larger than supported"));

        size_t offset2 = static_cast<size_t>(offset);

        switch (origin)
        {
            case stream_origin::begin:
                newPos = offset2;
                break;

            case stream_origin::current: 
                if (-offset > static_cast<long long>(_curPos))
                {
                    // Attempt to seek before beginning of buffer
                    BAJA_THROW(std::logic_error("attempt to seek before beginning of buffer")); // ERROR:
                }
                else
                {
                    newPos = _curPos + offset2;
                }
                break;

            case stream_origin::end:
                if (-offset > static_cast<long long>(_buffer.size()))
                {
                    // Attempt to seek before beginning of buffer
                    BAJA_THROW(std::logic_error("attempt to seek before beginning of buffer")); // ERROR:
                }
                else
                {
                    newPos = _buffer.size() + offset2;
                }
                break;
        
        default:
            BAJA_THROW(std::logic_error("")); // ERROR:
            break;
        }

        if (newPos > _buffer.size())
        {
            // Attempt to seek after end of buffer
            BAJA_THROW(std::logic_error("attempt to seek after end of buffer")); // ERROR:
        }

        // Move buffer pointer.  Cast is safe because of previous bounds check.
        _curPos = newPos;

        return _curPos;
    }

    void commit(const uint32 commitFlags)
    {
        // Not needed for a memory stream
        return;
    }

    void revert(void) 
    {
        BAJA_THROW(std::logic_error("nyi")); // ERROR:
    }

    void lockRegion(const size_t offset, const size_t cb, const uint32 lockType)
    {
        BAJA_THROW(std::logic_error("nyi")); // ERROR:
    }

    void unlockRegion(const size_t offset, const size_t cb, const uint32 lockType)
    {
        BAJA_THROW(std::logic_error("nyi")); // ERROR:
    }

    void copyTo(const std::shared_ptr<istream>& s, const size_t cb, size_t* cbRead, size_t* cbWritten)
    {
        BAJA_THROW(std::logic_error("nyi")); // ERROR:
    }

    // imemory_stream
    
    const std::vector<byte>& getBytes() { return _buffer; }

    void* getBuffer()
    {
        return _buffer.data();
    }

    size_t getBufferSize()
    {
        return _buffer.size();
    }

    size_t getBufferPos()
    {
        return _curPos;
    }
};

inline std::shared_ptr<imemory_stream> createMemoryStream(
    const void* data,
    const size_t dataSize
    )
{
    std::shared_ptr<memory_stream> returnValue = memory_stream::create(data, dataSize);
    return std::dynamic_pointer_cast<imemory_stream>(returnValue);
}

#ifdef BAJA_WINRT

using namespace Concurrency;
using namespace Platform;
using namespace Windows::Storage;
using namespace Windows::Storage::Streams;
using namespace Windows::Foundation;

inline task<std::shared_ptr<imemory_stream>> 
createMemoryStreamFromFileAsync(const wchar* fileName)
{
    task<StorageFile^> getFileTask;

    getFileTask = task<StorageFile^>(StorageFile::GetFileFromPathAsync(ref new Platform::String(fileName)));

    std::shared_ptr<DataReader^> context = std::make_shared<DataReader^>(nullptr);

    return getFileTask.then([](StorageFile^ storageFile) -> IAsyncOperation<IRandomAccessStream^>^{

        return storageFile->OpenAsync(Windows::Storage::FileAccessMode::Read);

    }).then([context](IRandomAccessStream^ stream) mutable -> IAsyncOperation<unsigned int>^ 
    {
        *context = ref new DataReader(stream); // use GetInputStreamAt(0)) to implement Seek (doesn't work in the current build of windows)
        
        return (*context)->LoadAsync(static_cast<baja::uint32>(stream->Size));

    }).then([context](task<unsigned int> t) -> std::shared_ptr<imemory_stream>
    {
        unsigned int size = t.get();

        Platform::Array<unsigned char>^ buffer = ref new Platform::Array<unsigned char>(static_cast<uint32>(size));
        uint32 bytesRead = static_cast<uint32>(size);

        (*context)->ReadBytes(buffer);

        std::shared_ptr<istream> ms = createMemoryStream((void*)nullptr, 0);

        // now copy it to the memory stream
        size_t bytesWritten = 0;
        bytesWritten = ms->write(buffer->begin(), bytesRead);
        BAJA_VERIFY(bytesWritten == bytesRead);

        return std::dynamic_pointer_cast<imemory_stream>(ms);
    });
}

// TRACK: 197: get rid of this
inline std::shared_ptr<imemory_stream> createMemoryStreamFromFile(
    const wchar* fileName
    )
{
    std::shared_ptr<istream> fs = createFileStream(fileName, open_mode::read);

    // read the entire file at once
    std::vector<char> buffer;
    const size_t bytesToRead = fs->getSize();
    buffer.resize(bytesToRead);

    std::shared_ptr<istream> ms = createMemoryStream((void*)nullptr, 0);

    size_t bytesRead = fs->read(buffer.data(), bytesToRead);
    BAJA_VERIFY(bytesRead == bytesToRead);

    // now copy it to the memory stream
    size_t bytesWritten = 0;
    bytesWritten = ms->write(buffer.data(), bytesRead);

    BAJA_VERIFY(bytesWritten == bytesRead);

    return std::dynamic_pointer_cast<imemory_stream>(ms);
}
#else
inline task<std::shared_ptr<imemory_stream>> 
createMemoryStreamFromFileAsync(const wchar* fileName)
{
    BAJA_THROW(std::logic_error("not implemented"));
    return task<std::shared_ptr<imemory_stream>>();
}

inline std::shared_ptr<imemory_stream> createMemoryStreamFromFile(
    const wchar* fileName
    )
{
    std::shared_ptr<istream> fs = createFileStream(fileName, open_mode::read);

    // 4k staging buffer to read the file into memory
    std::vector<char> buffer;
    const size_t bytesToRead = 4 * 1024;
    size_t bytesRead = 0;
    buffer.resize(bytesToRead);

    size_t totalSize = fs->getSize();

    std::shared_ptr<istream> ms = createMemoryStream((void*)nullptr, 0);

    fs->seek(0, baja::stream_origin::begin);
    bytesRead = fs->read(buffer.data(), bytesToRead);
    while (bytesRead > 0)
    {
        size_t bytesWritten = 0;
        bytesWritten = ms->write(buffer.data(), bytesRead);

        BAJA_VERIFY(bytesWritten == bytesRead);

        bytesRead = fs->read(buffer.data(), bytesToRead);
    }

    return std::dynamic_pointer_cast<imemory_stream>(ms);
}
#endif // BAJA_WINRT

} // namespace baja
